#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdbool.h>

#include "disk-array.h"
#define MAXSIZE 20

//TODO change dar_data to data

/////////////////////////////////////DATA//////////////////////////////////////

//declare variables in command
//RAID level
int level = -1;
//strip flag: # of blocks in strip
int blocks_in_strip = -1;
//disk flag: # of disks
int ndisks = -1;
//size flag: # of blocks in a disk
int nblocks = -1;
//strips per disk
int nstrips = -1;
//trace flag: filename for batch file
char *trace = NULL;
// file pointer
FILE *fp;
//verbose?
int verbose = 0;
// the disk array
disk_array_t da;

// Read function
typedef void (*read_func_t) (disk_array_t da, int lba, int length, char *data);
// Write function
typedef void (*write_func_t) (disk_array_t da, int lba, int length, char *value);
// Recover function
typedef void (*recover_func_t) (disk_array_t da, int disk);

// pointers to command functions
read_func_t read_func;
write_func_t write_func;
recover_func_t recover_func;

int num_failed = 0;
int fail_disk = -1;
int had_two_failed = 0;
int error_count = 0;
char err[] = "ERROR";

/////////////////////////////////////CODE//////////////////////////////////////

// XOR two strings of length BLOCK_SIZE and put into same string
// Parameter sum: the first value to XOR and where to put the result
// Parameter new: the second value to XOR
void xor_block(char* sum, char* new){
        int j;
        // XOR each byte
        for(j = 0; j < BLOCK_SIZE; j++) {
                sum[j] = sum[j] ^ new[j];
        }
}

// Should only be used with systems with a parity disk
// Parameter disk: disk to find value for
// Parameter block: which block in the strip we should use
// Parameter data: where the value should be put
// Return: 0 if success, -1 if failed (block larger than nblocks)
int orig_val(disk_array_t da, int disk, int block, char *data) {

        if(block >= nblocks) {
                return(-1);
        }
        // clear the data to be empty
        xor_block(data, data);      
        //XOR all remaining disks and the Parity to get value
        char *partial = malloc(BLOCK_SIZE);
        int i;
        for(i = 0; i < ndisks; i++) {
                if(i != disk) {
                        if(disk_array_read(da, i, block, partial) < 0) continue;
                        if(data == NULL) {
                                // SHOULDN'T HAPPEN
                                printf("SHOULDN'T HAPPEN\n");
                                // the first block to XOR
                                data = malloc(BLOCK_SIZE);
                                strcpy(data, partial);
                        } else {
                                // XOR with remaining blocks
                                xor_block(data, partial);
                        }
                }
        }
        free(partial);
        return(0);
}

//READ LBA SIZE - read starting at block LBA for SIZE(length) blocks. Prints out the first 4 byte value in each block.
 
//read_func RAID 0
void read0(disk_array_t da, int lba, int length, char *data){

        char *dar_data = malloc(BLOCK_SIZE); //disk_array_read data
        int cur_block;
        int disk;
        int block;
        int offset;

        for(cur_block = lba; cur_block < length + lba; cur_block++){

		// Get the disk cur_block is on
		// get global disk-strip number (cur_block / blocks_in_strip)
		// collapse to actual disk (... % ndisks)
		disk = (cur_block / blocks_in_strip) % ndisks;
	   
		// Get cur_block's actual block within the disk
		//normalize to disk 0
		block = cur_block - (blocks_in_strip * disk);
		//normalize to first block in strip
		offset = block % blocks_in_strip;
		block = block - offset;
		//normalize to generic disk with nblocks:
		block = block / ndisks;
		//restore to the block in the strip
		block = block + offset;

                if(disk_array_read(da, disk, block, dar_data) < 0) {
                        // Disk is failed
                        printf("%s ",err);
                } else {
		        int out = *((int *) dar_data);
		        printf("%d ",out);
                }
        }
        free(dar_data);
}

//read_func RAID 10
void read10(disk_array_t da, int lba, int length, char *data){

        char *dar_data = malloc(BLOCK_SIZE); //disk_array_read data
        int cur_block;
        int disk;
        int block;
        int offset;
       
        for(cur_block = lba; cur_block < length + lba; cur_block++){

		// Get the disk cur_block is on
		// get global disk-strip number
		disk = cur_block / blocks_in_strip;
		// collapse to disk if we had no mirroring
		disk = disk % (ndisks / 2);
		// restore mirroring
		disk = disk * 2;
	   
		// Get cur_block's actual block within the disk
		//normalize to disk 0
		block = cur_block - (blocks_in_strip * (disk / 2));
		//normalize to first block in strip
		offset = block % blocks_in_strip;
		block = block - offset;
		//normalize to generic disk with nblocks:
		block = block / (ndisks / 2);
		//restore to the block in the strip
		block = block + offset;
               
                if(disk_array_read(da, disk, block, dar_data) < 0) {
                        if(disk_array_read(da, disk + 1, block, dar_data) < 0) {
                        	// Disk is failed
                        	printf("%s ",err);
                        	continue;
                        }
                }
                
                int out = *((int *) dar_data);
                printf("%d ",out);
        }
        free(dar_data);
}

//read_func RAID 4
void read4(disk_array_t da, int lba, int length, char *data){

        char *dar_data = malloc(BLOCK_SIZE); //disk_array_read data
        int cur_block;
        int disk;
        int block;
        int offset;

        for(cur_block = lba; cur_block < length + lba; cur_block++){
               
                // Get the disk cur_block is on
                // get global disk-strip number
                disk = cur_block / blocks_in_strip;
                // collapse to actual disk (excluding parity disk)
                disk = disk % (ndisks - 1);

                // Get cur_block's actual block within the disk
                //normalize to disk 0
                block = cur_block - (blocks_in_strip * disk);
                //normalize to first block in strip
                offset = block % blocks_in_strip;
                block = block - offset;
                //normalize to generic disk with nblocks:
                block = block / (ndisks - 1);
                //restore to the block in the strip
                block = block + offset;

                if(disk_array_read(da, disk, block, dar_data) == -1) {
                        // Disk is failed
                        if(num_failed >= 2) {
                        	printf("%s ",err);
                        	continue;
                        } else {
                        	orig_val(da, disk, block, dar_data);
                        }
                }
                
                int out = *((int *) dar_data);
                printf("%d ",out);
        }
        free(dar_data);
}

int find_actual_disk(int disk, int lba) {
        int blocks_in_row = (ndisks - 1) * blocks_in_strip;
        int row = lba / blocks_in_row;
        int par_disk = row % ndisks;
        if(disk >= par_disk) {
                disk++;
        }
        return(disk);
}

int find_par_disk(int lba){
        int blocks_in_row = (ndisks - 1) * blocks_in_strip;
        int row = lba / blocks_in_row;
        int par_disk = row % ndisks;
        return(par_disk);
}

//read_func RAID 5
void read5(disk_array_t da, int lba, int length, char *data){
         
        char *dar_data = malloc(BLOCK_SIZE); //disk_array_read data
        int cur_block;
        int disk;
        int block;
        int offset;
	
        for(cur_block = lba; cur_block < length + lba; cur_block++){
               
                // Get the disk cur_block is on
                // get global disk-strip number
                disk = cur_block / blocks_in_strip;
                // collapse to actual disk (excluding parity disk)
                disk = disk % (ndisks - 1);

                // Get cur_block's actual block within the disk
                //normalize to disk 0
                block = cur_block - (blocks_in_strip * disk);
                //normalize to first block in strip
                offset = block % blocks_in_strip;
                block = block - offset;
                //normalize to generic disk with nblocks:
                block = block / (ndisks - 1);
                //restore to the block in the strip
                block = block + offset;
               
                // find actual disk because of parity disk offset
                disk = find_actual_disk(disk, cur_block);
                
                if(disk_array_read(da, disk, block, dar_data) == -1) {
                        // Disk is failed
                        if(num_failed >= 2) {
                        	printf("%s ",err);
                        	continue;
                        } else {
                        	orig_val(da, disk, block, dar_data);
                        }
                }
		
		int out = *((int *) dar_data);
                printf("%d ",out);
        }
        free(dar_data);
}



//WRITE LBA SIZE VALUE - write the 4-byte pattern VALUE repeatedly starting at block LBA for SIZE(length) blocks.

//write_func RAID 0
void write0(disk_array_t da, int lba, int length, char *value){

        int cur_block; //current global disk-block for reading
        int disk;
        int block;
        int offset;
       
        for(cur_block = lba; cur_block < length + lba; cur_block++){

		// Get the disk cur_block is on
		// get global disk-strip number (cur_block / blocks_in_strip)
		// collapse to actual disk (... % ndisks)
		disk = (cur_block / blocks_in_strip) % ndisks;
	       
		// Get cur_block's actual block within the disk
		//normalize to disk 0
		block = cur_block - (blocks_in_strip * disk);
		//normalize to first block in strip
		offset = block % blocks_in_strip;
		block = block - offset;
		//normalize to generic disk with nblocks:
		block = block / ndisks;
		//restore to the block in the strip
		block = block + offset;

                if(disk_array_write(da, disk, block, value) < 0) {
                        // Disk is failed
                        error_count++;
                }
        }
        
        // print error
        if(error_count > 0) printf("%s ",err);
        error_count = 0;
}

//write_func RAID 10
void write10(disk_array_t da, int lba, int length, char *value){

        int cur_block; //current global disk-block for reading
        int disk;
        int block;
        int offset;
       
        for(cur_block = lba; cur_block < length + lba; cur_block++){

		// Get the disk cur_block is on
		// get global disk-strip number
		disk = cur_block / blocks_in_strip;
		// collapse to disk if we had no mirroring
		disk = disk % (ndisks / 2);
		// restore mirroring
		disk = disk * 2;
	       
		// Get cur_block's actual block within the disk
		//normalize to disk 0
		block = cur_block - (blocks_in_strip * (disk / 2));
		//normalize to first block in strip
		offset = block % blocks_in_strip;
		block = block - offset;
		//normalize to generic disk with nblocks:
		block = block / (ndisks / 2);
		//restore to the block in the strip
		block = block + offset;
		
		int write_err = 0;
                if(disk_array_write(da, disk, block, value) < 0) {
                        // Disk is failed
                        write_err++;
                }
                if(disk_array_write(da, disk + 1, block, value) < 0) {
                        // Disk is failed
                        write_err++;
                }
                if(write_err >= 2) error_count++;
        }
        
        // print error
        if(error_count > 0) printf("%s ",err);
        error_count = 0;
}

//write_func RAID 4
void write4(disk_array_t da, int lba, int length, char *value){

        int cur_block; //current global disk-block for reading
        int disk;
        int block;
        int offset;
        int end_lba = length + lba;
        char* orig = malloc(BLOCK_SIZE);
        char* par_val = malloc(BLOCK_SIZE);
        
        for(cur_block = lba; cur_block < end_lba; cur_block++){

		// Get the disk cur_block is on
		// get global disk-strip number
		disk = cur_block / blocks_in_strip;
		// collapse to actual disk (excluding parity disk)
		disk = disk % (ndisks - 1);

		// Get cur_block's actual block within the disk
		//normalize to disk 0
		block = cur_block - (blocks_in_strip * disk);
		//normalize to first block in strip
		offset = block % blocks_in_strip;
		block = block - offset;
		//normalize to generic disk with nblocks:
		block = block / (ndisks - 1);
		//restore to the block in the strip
		block = block + offset;
               
                // read what the old value is for the block we are writing to
                if(disk_array_read(da, disk, block, orig) < 0) {
                        // if the disk is failed, need to calculate the original value
                        orig_val(da, disk, block, orig);
                }
                // Write to the block
                if(disk_array_write(da, disk, block, value) == -1) {
                        // Disk is failed
                        error_count++;
                }
               
                xor_block(par_val,par_val); // clear temp parity value
                // recalculate parity for each write (subtractive parity)
                if(disk_array_read(da, ndisks - 1, block, par_val) >= 0) {
                        xor_block(par_val, orig); // subtract old value
                        xor_block(par_val, value); // add in new value
                        if(disk_array_write(da, ndisks - 1, block, par_val) < 0) {
                        	error_count++;
                        }
                }
        }     
           
        // print error
        if(error_count > 0) printf("%s ",err);
        error_count = 0;
        
        free(orig);
        free(par_val);
}

//write_func RAID 5
void write5(disk_array_t da, int lba, int length, char *value){

        int cur_block; //current global disk-block for reading
        int disk;
        int block;
        int offset;
        int end_lba = length + lba;
        char* orig = malloc(BLOCK_SIZE);
        char* par_val = malloc(BLOCK_SIZE);
        int par_disk;
        
        for(cur_block = lba; cur_block < end_lba; cur_block++){

		// Get the disk cur_block is on
		// get global disk-strip number
		disk = cur_block / blocks_in_strip;
		// collapse to actual disk (excluding parity disk)
		disk = disk % (ndisks - 1);

		// Get cur_block's actual block within the disk
		//normalize to disk 0
		block = cur_block - (blocks_in_strip * disk);
		//normalize to first block in strip
		offset = block % blocks_in_strip;
		block = block - offset;
		//normalize to generic disk with nblocks:
		block = block / (ndisks - 1);
		//restore to the block in the strip
		block = block + offset;
                       
                // find actual disk because of parity disk offset
                disk = find_actual_disk(disk, cur_block);
               
                // read what the old value is for the block we are writing to
                if(disk_array_read(da, disk, block, orig) < 0) {
                        // if the disk is failed, need to calculate the original value
                        
                        orig_val(da, disk, block, orig);
                }
                // Write to the block
                if(disk_array_write(da, disk, block, value) == -1) {
                        // Disk is failed
                        error_count++;
                }
               
                // recalculate parity for each write (subtractive parity)
                xor_block(par_val,par_val); // clear temp parity value
                par_disk = find_par_disk(cur_block);
                if(disk_array_read(da, par_disk, block, par_val) >= 0) {
                        xor_block(par_val, orig); // subtract old value
                        xor_block(par_val, value); // add in new value
                        if(disk_array_write(da, par_disk, block, par_val) < 0) {
                        	error_count++;
                        }
                }
        }
                
        // print error
        if(error_count > 0) printf("%s ",err);
        error_count = 0;
        
        free(orig);
        free(par_val);
}



//rec_func RAID 0
void recover0(disk_array_t da, int disk){
        disk_array_recover_disk(da, disk);
        //can't recover from RAID 0
}
//rec_func RAID 10
void recover10(disk_array_t da, int disk){
        int recover_disk;

        disk_array_recover_disk(da, disk);

        // find disk number to recover from
        if(disk % 2 == 0) {
                // disk is even, recover disk is disk + 1
                recover_disk = disk + 1;
                
        } else {
                // disk is odd, recover disk is disk - 1
                recover_disk = disk - 1;
        }

        // copy all values from mirrored disk to current disk
        int i;
        char *temp = malloc(BLOCK_SIZE);
        for(i = 0; i < nblocks; i++) {
        	if( disk_array_read(da, recover_disk, i, temp) < 0) {
        		return;
        	}
                disk_array_write(da, disk, i, temp);
        }
        free(temp);
}

void recalc_parity(disk_array_t da) {

	// find parity disk for RAID 4
	int parity_disk;
	if(level == 4) parity_disk = ndisks - 1;
	
	char *temp = malloc(BLOCK_SIZE);
        // for each block, find original value and copy it to the disk
        int i;
        for(i = 0; i < nblocks; i++) {
        
		// find parity disk for RAID 5
		if(level == 5) parity_disk = find_par_disk(i);
	
		// replace value
                orig_val(da, parity_disk, i, temp);
                disk_array_write(da, parity_disk, i, temp);
        }
        free(temp);
}

//rec_func RAID 4
void recover4(disk_array_t da, int disk){
        disk_array_recover_disk(da, disk);
        
        if(num_failed > 0) {
        	// can't recover
        	return;
        }
        
        // if there were two failed disks and currently all are recovered, need
        // to recalculate parity
        if(num_failed == 0 && had_two_failed == 1) {
        	recalc_parity(da);
        	had_two_failed = 0;
        	return;
        }
        
        char *temp = malloc(BLOCK_SIZE);
        // for each block, find original value and copy it to the disk
        int i;
        for(i = 0; i < nblocks; i++) {
                orig_val(da, disk, i, temp);
                disk_array_write(da, disk, i, temp);
        }
        free(temp);
}

//rec_func RAID 5
void recover5(disk_array_t da, int disk){
        disk_array_recover_disk(da, disk);
        
        if(num_failed > 0) {
        	// can't recover
        	return;
        }
        
        // if there were two failed disks and currently all are recovered, need
        // to recalculate parity
        if(num_failed == 0 && had_two_failed == 1) {
        	recalc_parity(da);
        	had_two_failed = 0;
        	return;
        }
       
        char *temp = malloc(BLOCK_SIZE);
        // for each block, find original value and copy it to the disk
        int i;
        for(i = 0; i < nblocks; i++) {
                orig_val(da, disk, i, temp);
                disk_array_write(da, disk, i, temp);
        }
        free(temp);
}



int main(int argc, char *argv[]){
               
        //get commands
        char *c; // flag
        int i; // iteration variable
        for(i = 1; i < argc; i++) {
                c = argv[i];
                if(strcmp(c, "-level") == 0) {
                        level = atoi(argv[++i]);
                        if(level != 0 && level != 10 && level != 4 && level != 5) {
                                exit(1);
                        }
                } else if(strcmp(c, "-strip") == 0) {
                        blocks_in_strip = atoi(argv[++i]);
                } else if(strcmp(c, "-disks") == 0) {
                        ndisks = atoi(argv[++i]);
                } else if(strcmp(c, "-size") == 0) {
                        nblocks = atoi(argv[++i]);
                } else if(strcmp(c, "-trace") == 0) {
                        trace = strdup(argv[++i]);
                } else if(strcmp(c, "-verbose") == 0) {
                        verbose = 1;
                } else {
                        exit(1);
                }
        }
       
        // check if mandatory parameters were provided and valid
        // default values are -1
        if(level < 0  || blocks_in_strip < 0 || ndisks < 0 || nblocks < 0 || trace == NULL) {
                exit(1);
        }
       
        // check number of disks requirement
        if(level == 10 && ndisks % 2 != 0) {
                exit(1);
        }
        if((level == 4 || level == 5) && ndisks < 3) {
                exit(1);
        }
       
        // open the file of disk commands
        fp = fopen(trace, "r");
        if(fp == NULL) {
                // file error occurred
                exit(1);
        }

        //disk array
        da = disk_array_create("myvirtualdisk", ndisks, nblocks);
       
        //finding # of strips in a disk
        nstrips = nblocks / blocks_in_strip;
       
        // assign RAID functions
        if(level == 0) {
                read_func = read0;
                write_func = write0;
                recover_func = recover0;
        } else if(level == 10) {
                read_func = read10;
                write_func = write10;
                recover_func = recover10;
        } else if(level == 4) {
                read_func = read4;
                write_func = write4;
                recover_func = recover4;
        } else if(level == 5) {
                read_func = read5;
                write_func = write5;
                recover_func = recover5;
        } else {
                printf("RAID not 0, 10, 4, or 5 - SHOULDN'T HAPPEN\n");
        }
       
        //code for parsing trace file
        //in this code we'll call the above functions
                // at most MAXSIZE characters each line in trace
        char line[MAXSIZE];
        // tokens in each line
        char * token;
        // data we read in
        char * data = malloc(BLOCK_SIZE);
        char * value = malloc(4);
        int lba, size, disk;
       
        while (fgets(line, MAXSIZE, fp)!= NULL){
          printf("%s",line);
          token = strtok(line, " \n");
          if (strcmp(token, "READ") == 0){
            token = strtok(NULL, " \n");
            lba = atoi(token);
            token = strtok(NULL, " \n");
            size = atoi(token);
            if ( strtok(NULL, " \n") != NULL){
              printf("Wrong Command Format!\n");
              continue;
            }
            read_func(da, lba, size, data);
            continue;
          }
          if (strcmp(token, "WRITE") == 0){
            token = strtok(NULL, " \n");
            lba = atoi(token);
            token = strtok(NULL, " \n");
            size = atoi(token);
            token = strtok(NULL, " \n");
            int i = atoi(token);
            value = (char*) &i;
            if (strtok(NULL, " \n") != NULL){
              printf("Wrong Command Format!\n");
              continue;
            }
            write_func(da, lba, size, value);
            continue;
          }
          if (strcmp(token, "FAIL") == 0){
            token = strtok(NULL, " \n");
            disk = atoi(token);
            if (strtok(NULL, " \n") != NULL){
              printf("Wrong Command Format!\n");
              continue;
            }
            num_failed++;
            if(num_failed >= 2) had_two_failed = 1;
            disk_array_fail_disk(da, disk);
            fail_disk = disk;
            continue;
          }
          if (strcmp(token, "RECOVER") == 0){
            token = strtok(NULL, " \n");
            disk = atoi(token);
            if (strtok(NULL, " \n") != NULL){
              printf("Wrong Command Format!\n");
              continue;
            }
            num_failed--;
            recover_func(da, disk);
            fail_disk = -1;
            continue;
          }
          if (strcmp(token, "END") == 0){
            token = strtok(NULL, " \n");
            if (strtok(NULL, " \n") != NULL){
              printf("Wrong Command Format!\n");
              continue;
            }
            break;
          }
          printf("Wrong Command Format!\n");
        }
        
        disk_array_print_stats(da);
        disk_array_close(da);
       
        exit(0);
}

